/*
 * Copyright 1999,2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.webduff.methods;

import org.w3c.dom.Node;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.googlecode.webduff.WebdavStatus;
import com.googlecode.webduff.exceptions.AccessDeniedException;
import com.googlecode.webduff.exceptions.MethodResponseError;
import com.googlecode.webduff.exceptions.WebdavException;
import com.googlecode.webduff.io.URI;
import com.googlecode.webduff.locking.AcquiredLock;
import com.googlecode.webduff.locking.LockFailedException;
import com.googlecode.webduff.util.URLEncoder;
import com.googlecode.webduff.util.XMLWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletRequest;
import javax.servlet.ServletException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.util.*;
import java.io.IOException;
import java.text.SimpleDateFormat;

public class DoPropfind extends AbstractMethod {

	private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(DoPropfind.class);

    /**
     * Array containing the safe characters set.
     */
    protected static URLEncoder urlEncoder;

    /**
     * Simple date format for the creation date ISO representation (partial).
     */
    protected static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    /**
     * Default depth is infinite.
     */
    private static final int INFINITY = 3;

    /**
     * PROPFIND - Specify a property mask.
     */
    private static final int FIND_BY_PROPERTY = 0;

    /**
     * PROPFIND - Display all properties.
     */
    private static final int FIND_ALL_PROP = 1;

    /**
     * PROPFIND - Return property names.
     */
    private static final int FIND_PROPERTY_NAMES = 2;

    static {
        creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        /**
         * GMT timezone - all HTTP dates are on GMT
         */
        urlEncoder = new URLEncoder();
        urlEncoder.addSafeCharacter('-');
        urlEncoder.addSafeCharacter('_');
        urlEncoder.addSafeCharacter('.');
        urlEncoder.addSafeCharacter('*');
        urlEncoder.addSafeCharacter('/');
    }

    public MethodResponse execute(HttpServletRequest req, HttpServletResponse resp) throws IOException, MethodResponseError {
    	URI theURI = getURIForRequest(req);
        MethodResponse theResponse = new MethodResponse(resp);
        AcquiredLock acquiredLock = null;
        int depth = getDepth(req);
        
        try {
			acquiredLock = acquireSharedLock(theURI, depth, req);
		} catch (LockFailedException e) {
			throw new MethodResponseError(WebdavStatus.SC_LOCKED);
		}
		
		try {
            if (!store.objectExists(theURI)) {
                log.debug("DoPropfind: object doesn't exists!");
                return theResponse.setStatusCode(WebdavStatus.SC_NOT_FOUND);
            }

            Vector<String> properties = null;

            int propertyFindType = FIND_ALL_PROP;
            Node propNode = null;
            getPropertyNodeAndType(propNode, propertyFindType, req);

            if (propertyFindType == FIND_BY_PROPERTY) {
                properties = getPropertiesFromXML(propNode);
            }
            
            theResponse = theResponse.setStatusCode(WebdavStatus.SC_MULTI_STATUS).setContentType("text/xml; charset=UTF-8");

            // Create multistatus object
            XMLWriter generatedXML = new XMLWriter(theResponse.getWriter());
            generatedXML.writeXMLHeader();
            generatedXML.writeElement(null, "multistatus xmlns=\"DAV:\"", XMLWriter.OPENING);
            if (depth == 0) {
                parseProperties(req, generatedXML, theURI, propertyFindType, properties, theMimeTyper.getMimeType(theURI));
            } else {
                recursiveParseProperties(theURI, req, generatedXML, propertyFindType, properties, depth, theMimeTyper.getMimeType(theURI));
            }
            generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
            generatedXML.sendData();
        } catch (AccessDeniedException e) {
        	log.trace("DoPropfind(" + theURI.getPath() + ")", e);
            throw new MethodResponseError(WebdavStatus.SC_FORBIDDEN);
        } catch (WebdavException e) {
        	log.debug("DoPropfind(" + theURI.getPath() + ")", e);
        	throw new MethodResponseError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
        } catch (ServletException e) {
        	log.trace("DoPropfind(" + theURI.getPath() + ")");
        	throw new MethodResponseError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
        } catch(Throwable t) {
        	log.debug("DoPropfind(" + theURI.getPath() + ")", t);
        } finally {
            acquiredLock.unlock();
        }
        
        return theResponse;
    }

    /**
     * reads the depth header from the request and returns it as a int
     *
     * @param req
     * @return the depth from the depth header
     */
    private int getDepth(HttpServletRequest req) {
        int depth = INFINITY;
        String depthStr = req.getHeader("Depth");
        if (depthStr != null) {
            if (depthStr.equals("0")) {
                depth = 0;
            } else if (depthStr.equals("1")) {
                depth = 1;
            } else if (depthStr.equals("infinity")) {
                depth = INFINITY;
            }
        }
        return depth;
    }

    /**
     * overwrites propNode and type, parsed from xml input stream
     *
     * @param propNode
     * @param type
     * @param req
     *            HttpServletRequest
     * @throws javax.servlet.ServletException
     */
    private void getPropertyNodeAndType(Node propNode, int type,
            ServletRequest req) throws ServletException {
        if (req.getContentLength() != 0) {
            DocumentBuilder documentBuilder = getDocumentBuilder();
            try {
                Document document = documentBuilder.parse(new InputSource(req
                        .getInputStream()));
                // Get the root element of the document
                Element rootElement = document.getDocumentElement();
                NodeList childList = rootElement.getChildNodes();

                for (int i = 0; i < childList.getLength(); i++) {
                    Node currentNode = childList.item(i);
                    switch (currentNode.getNodeType()) {
                    case Node.TEXT_NODE:
                        break;
                    case Node.ELEMENT_NODE:
                        if (currentNode.getNodeName().endsWith("prop")) {
                            type = FIND_BY_PROPERTY;
                            propNode = currentNode;
                        }
                        if (currentNode.getNodeName().endsWith("propname")) {
                            type = FIND_PROPERTY_NAMES;
                        }
                        if (currentNode.getNodeName().endsWith("allprop")) {
                            type = FIND_ALL_PROP;
                        }
                        break;
                    }
                }
            } catch (Exception e) {

            }
        } else {
            // no content, which means it is a allprop request
            type = FIND_ALL_PROP;
        }
    }

    /**
     * Return JAXP document builder instance.
     */
    private DocumentBuilder getDocumentBuilder() throws ServletException {
        DocumentBuilder documentBuilder = null;
        DocumentBuilderFactory documentBuilderFactory = null;
        try {
            documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new ServletException("jaxp failed");
        }
        return documentBuilder;
    }

    private Vector<String> getPropertiesFromXML(Node propNode) {
        Vector<String> properties = new Vector<String>();
        NodeList childList = propNode.getChildNodes();

        for (int i = 0; i < childList.getLength(); i++) {
            Node currentNode = childList.item(i);
            switch (currentNode.getNodeType()) {
            case Node.TEXT_NODE:
                break;
            case Node.ELEMENT_NODE:
                String nodeName = currentNode.getNodeName();
                String propertyName = null;
                if (nodeName.indexOf(':') != -1) {
                    propertyName = nodeName
                            .substring(nodeName.indexOf(':') + 1);
                } else {
                    propertyName = nodeName;
                }
                // href is a live property which is handled differently
                properties.addElement(propertyName);
                break;
            }
        }
        return properties;
    }

    /**
     * goes recursive through all folders. used by propfind
     *
     * @param currentURI
     *            the current path
     * @param req
     *            HttpServletRequest
     * @param generatedXML
     * @param propertyFindType
     * @param properties
     * @param depth
     *            depth of the propfind
     * @throws IOException
     *             if an error in the underlying store occurs
     */
    private void recursiveParseProperties(URI currentURI,
            HttpServletRequest req, XMLWriter generatedXML,
            int propertyFindType, Vector<String> properties, int depth, String mimeType)
            throws WebdavException {

        parseProperties(req, generatedXML, currentURI, propertyFindType, properties, mimeType);
        String[] names = store.getChildrenNames(currentURI);
        if ((names != null) && (depth > 0)) {
            for (int i = 0; i < names.length; i++) {
                recursiveParseProperties(currentURI.append(names[i]), req, generatedXML, propertyFindType, properties, depth - 1, mimeType);
            }
        }
    }

    /**
     * Propfind helper method.
     *
     * @param req
     *            The servlet request
     * @param generatedXML
     *            XML response to the Propfind request
     * @param uri
     *            Path of the current resource
     * @param type
     *            Propfind type
     * @param propertiesVector
     *            If the propfind type is find properties by name, then this
     *            Vector contains those properties
     */
    private void parseProperties(HttpServletRequest req,
            XMLWriter generatedXML, URI uri, int type,
            Vector<String> propertiesVector, String mimeType) throws WebdavException {

        String creationdate = getISOCreationDate(store.getCreationDate(uri).getTime());
        boolean isFolder = store.isFolder(uri);
        SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
        String lastModified = formatter.format(store.getLastModified(uri));
        String resourceLength = String.valueOf(store.getResourceLength(uri));

        // ResourceInfo resourceInfo = new ResourceInfo(path, resources);

        generatedXML.writeElement(null, "response", XMLWriter.OPENING);
        String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " "
                + WebdavStatus.getStatusText(WebdavStatus.SC_OK));

        // Generating href element
        generatedXML.writeElement(null, "href", XMLWriter.OPENING);

		URI hrefURI = new URI(req.getContextPath());
		if (req.getServletPath() != null) {
			hrefURI = hrefURI.append(new URI(req.getServletPath()));
		}
		hrefURI = hrefURI.append(uri);
		
		String href = hrefURI.getPath();
		
        if (isFolder) {
            href = hrefURI.getFolderPath();
        }

        generatedXML.writeText(rewriteUrl(href));

        generatedXML.writeElement(null, "href", XMLWriter.CLOSING);

        String resourceName = uri.getLastComponent();

        switch (type) {
        case FIND_ALL_PROP:
            generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
            generatedXML.writeElement(null, "prop", XMLWriter.OPENING);

            generatedXML.writeProperty(null, "creationdate", creationdate);
            generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
            generatedXML.writeData(resourceName);
            generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
            
            if (!isFolder) {
                generatedXML.writeProperty(null, "getlastmodified", lastModified);
                generatedXML.writeProperty(null, "getcontentlength", resourceLength);
                String contentType = mimeType;
                if (contentType != null) {
                    generatedXML.writeProperty(null, "getcontenttype", contentType);
                }
                generatedXML.writeProperty(null, "getetag", getETag(uri, resourceLength, lastModified));
                generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
            } else {
                generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
                generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
                generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
            }
            
            generatedXML.writeProperty(null, "source", "");
            generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "status", XMLWriter.OPENING);
            generatedXML.writeText(status);
            generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);

            break;

        case FIND_PROPERTY_NAMES:

            generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
            generatedXML.writeElement(null, "prop", XMLWriter.OPENING);

            generatedXML.writeElement(null, "creationdate", XMLWriter.NO_CONTENT);
            generatedXML.writeElement(null, "displayname", XMLWriter.NO_CONTENT);
            if (!isFolder) {
                generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
                generatedXML.writeElement(null, "getcontentlength", XMLWriter.NO_CONTENT);
                generatedXML.writeElement(null, "getcontenttype", XMLWriter.NO_CONTENT);
                generatedXML.writeElement(null, "getetag", XMLWriter.NO_CONTENT);
                generatedXML.writeElement(null, "getlastmodified", XMLWriter.NO_CONTENT);
            }
            generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
            generatedXML.writeElement(null, "source", XMLWriter.NO_CONTENT);
            generatedXML.writeElement(null, "lockdiscovery", XMLWriter.NO_CONTENT);

            generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "status", XMLWriter.OPENING);
            generatedXML.writeText(status);
            generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);

            break;

        case FIND_BY_PROPERTY:
            Vector<String> propertiesNotFound = new Vector<String>();

            // Parse the list of properties
            generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
            generatedXML.writeElement(null, "prop", XMLWriter.OPENING);

            Enumeration<String> properties = propertiesVector.elements();

            while (properties.hasMoreElements()) {

                String property = (String) properties.nextElement();

                if (property.equals("creationdate")) {
                    generatedXML.writeProperty(null, "creationdate", creationdate);
                } else if (property.equals("displayname")) {
                    generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
                    generatedXML.writeData(resourceName);
                    generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
                } else if (property.equals("getcontentlanguage")) {
                    if (isFolder) {
                        propertiesNotFound.addElement(property);
                    } else {
                        generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
                    }
                } else if (property.equals("getcontentlength")) {
                    if (isFolder) {
                        propertiesNotFound.addElement(property);
                    } else {
                        generatedXML.writeProperty(null, "getcontentlength", resourceLength);
                    }
                } else if (property.equals("getcontenttype")) {
                    if (isFolder) {
                        propertiesNotFound.addElement(property);
                    } else {
                        generatedXML.writeProperty(null, "getcontenttype", mimeType);
                    }
                } else if (property.equals("getetag")) {
                    if (isFolder) {
                        propertiesNotFound.addElement(property);
                    } else {
                        generatedXML.writeProperty(null, "getetag", getETag(uri, resourceLength, lastModified));
                    }
                } else if (property.equals("getlastmodified")) {
                    if (isFolder) {
                        propertiesNotFound.addElement(property);
                    } else {
                        generatedXML.writeProperty(null, "getlastmodified", lastModified);
                    }
                } else if (property.equals("resourcetype")) {
                    if (isFolder) {
                        generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
                        generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
                        generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
                    } else {
                        generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
                    }
                } else if (property.equals("source")) {
                    generatedXML.writeProperty(null, "source", "");
                } else {
                    propertiesNotFound.addElement(property);
                }
            }

            generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "status", XMLWriter.OPENING);
            generatedXML.writeText(status);
            generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
            generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);

            Enumeration<String> propertiesNotFoundList = propertiesNotFound.elements();

            if (propertiesNotFoundList.hasMoreElements()) {
                status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND
                        + " "
                        + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND));

                generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
                generatedXML.writeElement(null, "prop", XMLWriter.OPENING);

                while (propertiesNotFoundList.hasMoreElements()) {
                    generatedXML.writeElement(null,
                            (String) propertiesNotFoundList.nextElement(),
                            XMLWriter.NO_CONTENT);
                }

                generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
                generatedXML.writeElement(null, "status", XMLWriter.OPENING);
                generatedXML.writeText(status);
                generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
                generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
            }
            break;
        }
        generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
    }

    /**
     * Get creation date in ISO format.
     *
     * @param creationDate
     *            the date in milliseconds
     * @return the Date in ISO format
     */
    private String getISOCreationDate(long creationDate) {
        StringBuffer creationDateValue = new StringBuffer(creationDateFormat.format(new Date(creationDate)));
        /*
         * int offset = Calendar.getInstance().getTimeZone().getRawOffset() /
         * 3600000; // FIXME ? if (offset < 0) { creationDateValue.append("-");
         * offset = -offset; } else if (offset > 0) {
         * creationDateValue.append("+"); } if (offset != 0) { if (offset < 10)
         * creationDateValue.append("0"); creationDateValue.append(offset +
         * ":00"); } else { creationDateValue.append("Z"); }
         */
        return creationDateValue.toString();
    }

    /**
     * Get the ETag associated with a file.
     *
     * @param path
     *            path to the resource
     * @param resourceLength
     *            filesize
     * @param lastModified
     *            last-modified date
     * @return the ETag
     */
    protected String getETag(URI uri, String resourceLength, String lastModified) {
        // TODO create a real (?) ETag
        // parameter "path" is not used at the monent
        return "W/\"" + resourceLength + "-" + lastModified + "\"";

    }

    /**
     * URL rewriter.
     *
     * @param path
     *            Path which has to be rewiten
     * @return the rewritten path
     */
    protected String rewriteUrl(String path) {
        return urlEncoder.encode(path);
    }
}